#include "main.h"

#ifdef __WIN32__
#include <windows.h>
#include <winnls.h>
#include <fileapi.h>
#include <shlobj.h>
#include <shellapi.h>
#include <wchar.h>
#include <sys/stat.h>
extern HINSTANCE hWinInstance;
extern HWND hwnd;
#else
#include <signal.h>
#define __USE_MISC
#include <dlfcn.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS 0x20
#endif
typedef enum
{
  GTK_FILE_CHOOSER_ACTION_OPEN,
  GTK_FILE_CHOOSER_ACTION_SAVE,
  GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
  GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER
} GtkFileChooserAction;
int kill(pid_t pid, int sig);
void (*gtk_init)(int *argc, char ***argv);
void* (*gtk_file_chooser_dialog_new)(const char *title, void *parent, int action, const char *first_button_text, ...);
int (*gtk_dialog_run)(void *dialog);
void (*gtk_widget_destroy)(void *widget);
char* (*gtk_file_chooser_get_filename)(void *chooser);
void (*gtk_file_chooser_set_do_overwrite_confirmation)(void *chooser, int do_overwrite_confirmation);
void (*gtk_file_chooser_set_create_folders)(void *chooser, int create_folders);
int (*gtk_file_chooser_set_current_folder)(void *chooser, const char *path);
int (*gtk_file_chooser_set_current_name)(void *chooser, const char *filename);
#endif

/**
 * Global file name (needed by modals)
 */
char fn[PATH_MAX + FILENAME_MAX + 1];
int pathlen = 0;

/**
 * Return true if we can handle this file format
 * but must be at least 32 bytes long
 */
int fileknown(uint8_t *buf)
{
    return sprites_detect(buf) || audio_detect(buf);
}

/**
 * Helper dispatcher to load a file into the approriate list
 */
int fileasset(int id, char *name, uint8_t *buf, uint64_t size)
{
    char *fn = NULL, *fn2 = NULL;
    int idx;

    if(!buf || size < 32) return 0;
    if(name) {
        if(name[0] == '/' || name[0] == '\\') name++;
        if((fn = strrchr(name, '/'))) {
            *fn = 0; if((fn2 = strrchr(name, '/'))) fn2++;
            *fn++ = '/';
        } else
        if((fn = strrchr(name, '\\'))) {
            *fn = 0; if((fn2 = strrchr(name, '\\'))) fn2++;
            *fn++ = '\\';
        } else fn = name;
    }
    if(sprites_detect(buf)) {
        if((idx = sprites_addsheet(id, name, buf, size)) != -1)
            sprites_add(id, name, sheets[idx].w, sheets[idx].h, 0, 0, idx, 0, 0, sheets[idx].w, sheets[idx].h, 0, -1);
    } else
    if(audio_detect(buf)) {
        if(!memcmp(buf, "MThd", 4) ||
            (name && (!memcmp(name, "bgm", 3) || !memcmp(name, "Bgm", 3) || !memcmp(name, "BGM", 3) ||
            !memcmp(name, "mus", 3) || !memcmp(name, "Mus", 3) || !memcmp(name, "MUS", 3))) ||
            (fn && (!memcmp(fn, "bgm", 3) || !memcmp(fn, "Bgm", 3) || !memcmp(fn, "BGM", 3) ||
            !memcmp(fn, "mus", 3) || !memcmp(fn, "Mus", 3) || !memcmp(fn, "MUS", 3))) ||
            (fn2 && (!memcmp(fn2, "bgm", 3) || !memcmp(fn2, "Bgm", 3) || !memcmp(fn2, "BGM", 3) ||
            !memcmp(fn2, "mus", 3) || !memcmp(fn2, "Mus", 3) || !memcmp(fn2, "MUS", 3))) ) {
            return audio_addbgm(id, name, buf, size) != -1 ? 1 : 0;
        } else {
            return audio_addsfx(id, name, buf, size) != -1 ? 1 : 0;
        }
    }
    return 0;
}

#ifdef __WIN32__
static void utf8_ucs2(char *fn, wchar_t *szFn)
{
    int i;
    memset(szFn, 0, 2*(PATH_MAX + FILENAME_MAX + 1));
    MultiByteToWideChar(CP_UTF8, 0, fn, -1, szFn, PATH_MAX + FILENAME_MAX);
    for(i = 0; szFn[i]; i++) if(szFn[i] == L'/') szFn[i] = L'\\';
}
#endif

/**
 * Create directory
 */
void filemkdir(char *fn)
{
#ifdef __WIN32__
    wchar_t szFn[PATH_MAX + FILENAME_MAX + 1];
    if(!fn || !*fn) return;
    utf8_ucs2(fn, szFn);
    _wmkdir(szFn);
#else
    if(!fn || !*fn) return;
    mkdir(fn, 0775);
#endif
}

/**
 * Open a file
 * size != NULL, read
 * size == NULL, write
 */
void *fileopen(char *fn, uint64_t *size)
{
#ifdef __WIN32__
    struct _stat64 st;
    wchar_t szFn[PATH_MAX + FILENAME_MAX + 1];
    HANDLE f;

    if(size) *size = 0;
    if(!fn || !*fn) return NULL;
    utf8_ucs2(fn, szFn);
    if(size) {
        if(_wstat64(szFn, &st) || !S_ISREG(st.st_mode)) return NULL;
        f = CreateFileW(szFn, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    } else
        f = CreateFileW(szFn, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if(f == INVALID_HANDLE_VALUE) f = NULL;
#else
    struct stat st;
    FILE *f;

    if(size) *size = 0;
    if(!fn || !*fn || (size && (stat(fn, &st) || !S_ISREG(st.st_mode)))) return NULL;
    f = fopen(fn, size ? "rb" : "wb");
#endif
    if(size) *size = (uint64_t)st.st_size;
    return (void*)f;
}

/**
 * Read from file
 */
uint64_t fileread(void *f, void *buf, uint64_t size)
{
#ifdef __WIN32__
    DWORD t;
    if(!f || !buf || !size) return 0;
    if(!ReadFile((HANDLE)f, buf, (DWORD)size, &t, NULL)) t = 0;
    return (uint64_t)t;
#else
    if(!f || !buf || !size) return 0;
    return (uint64_t)fread(buf, 1, (size_t)size, (FILE*)f);
#endif
}

/**
 * Write to file
 */
uint64_t filewrite(void *f, void *buf, uint64_t size)
{
#ifdef __WIN32__
    DWORD t;
    if(!f || !buf || !size) return 0;
    if(!WriteFile((HANDLE)f, buf, (DWORD)size, &t, NULL)) t = 0;
    return (uint64_t)t;
#else
    if(!f || !buf || !size) return 0;
    return (uint64_t)fwrite(buf, 1, size, (FILE*)f);
#endif
}

/**
 * Seek in file from the beginning
 */
void fileseek(void *f, uint64_t offs)
{
#ifdef __WIN32__
    LARGE_INTEGER pos;
    pos.QuadPart = offs;
    SetFilePointerEx((HANDLE)f, pos, NULL, FILE_BEGIN);
#else
    fseek((FILE*)f, offs, SEEK_SET);
#endif
}

/**
 * Close file
 */
void fileclose(void *f)
{
    if(f) {
#ifdef __WIN32__
        CloseHandle((HANDLE)f);
#else
        fclose((FILE*)f);
#endif
    }
}

/**
 * Return true if file exists and is actually a file
 */
int fileexists(char *fn)
{
    uint64_t size;
    void *f;

    if(!fn || !*fn || !(f = fileopen(fn, &size))) return 0;
    fileclose(f);
    return !!size;
}

/**
 * Read in a file into memory
 */
uint8_t* getfile(char *fn, uint64_t *size)
{
    uint8_t *buf = NULL;
    void *f;

    *size = 0;
    if(!fn || !*fn || !(f = fileopen(fn, size))) return NULL;
    debug("getfile '%s' %lu bytes", fn, *size);
    if((buf = (uint8_t*)malloc((*size) + 64))) {
        memset(buf, 0, (*size) + 64);
        *size = fileread(f, buf, *size);
        fileclose(f);
    } else {
        engine_features |= EF_NOMEM;
        debug("unable allocate memory for '%s' (%lu bytes)", fn, *size);
        fileclose(f);
    }
    return buf;
}

/**
 * Write out multiple buffers into a file
 */
int putfile(char *fn, uint8_t *buf1, uint64_t size1, uint8_t *buf2, uint64_t size2, uint8_t *buf3, uint64_t size3)
{
    void *f;

    if(!fn || !*fn || !buf1 || size1 < 1 || !(f = fileopen(fn, NULL))) return 0;
    debug("putfile '%s' %lu bytes", fn, size1 + size2 + size3);
    filewrite(f, buf1, size1);
    if(buf2 && size2 > 0) filewrite(f, buf2, size2);
    if(buf3 && size3 > 0) filewrite(f, buf3, size3);
    fileclose(f);
    return 1;
}

/**
 * Iterate on directory entries (subdirs == 1 recursively) and call a callback on each file
 */
int find(char *path, int subdirs, filecb_t filecb)
{
    int len;
#ifdef __WIN32__
    wchar_t szFn[PATH_MAX + FILENAME_MAX + 1];
    WIN32_FIND_DATAW ffd;
    HANDLE h;
#else
    DIR *dir;
    struct dirent *ent;
#endif
    uint64_t size;
    void *f;
    int ret = 1;

    if(!path || !*path || !filecb) return 0;
    if(path != fn) strcpy(fn, path);
    len = strlen(fn);
    if(len && (fn[len - 1] == '/' || fn[len - 1] == '\\')) fn[--len] = 0;
    if((f = fileopen(fn, &size))) { fileclose(f); return (*filecb)(fn); }
#ifdef __WIN32__
    strcpy(fn + len, "/*.*");
    utf8_ucs2(fn, szFn);
    if((h = FindFirstFileW(szFn, &ffd)) != INVALID_HANDLE_VALUE) {
        do {
            if(!wcscmp(ffd.cFileName, L".") || !wcscmp(ffd.cFileName, L".."))
                continue;
            fn[len] = '/';
            WideCharToMultiByte(CP_UTF8, 0, ffd.cFileName, -1, fn + len + 1, FILENAME_MAX, NULL, NULL);
            if(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                if(!subdirs) continue;
                find(fn, 1, filecb);
            } else {
                ret = (*filecb)(fn);
            }
        } while(!quit && ret && FindNextFileW(h, &ffd) != 0);
        FindClose(h);
    }
#else
    if((dir = opendir(path)) != NULL) {
        while(!quit && ret && (ent = readdir(dir)) != NULL) {
            if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, ".."))
                continue;
            fn[len] = '/';
            strncpy(fn + len + 1, ent->d_name, FILENAME_MAX);
            if(ent->d_type == DT_DIR) {
                if(!subdirs) continue;
                find(fn, 1, filecb);
            } else
            if(ent->d_type == DT_REG) {
                ret = (*filecb)(fn);
            }
        }
        closedir(dir);
    }
#endif
    return ret;
}

static uint64_t total;

/**
 * Callback for disk usage
 */
int ducb(char *fn)
{
    uint64_t size = 0;
    void *f = fileopen(fn, &size);
    if(f) fileclose(f);
    total += size;
    return 1;
}

/**
 * Calculate disk usage for a directory or file
 */
uint64_t du(char *path)
{
    if(!path || !*path) return 0;
    total = 0;
    find(path, 1, ducb);
    return total;
}

/**
 * Helper for number of files in directory
 */
int dncb(char *fn)
{
    uint64_t size = 0;
    void *f = fileopen(fn, &size);
    if(f) { total++; fileclose(f); }
    return 1;
}

/**
 * Return number of files in a directory
 */
uint64_t dn(char *path)
{
    if(!path || !*path) return 0;
    total = 0;
    find(path, 1, dncb);
    return total;
}

/**
 * Open a select folder or open file modal
 */
void opendialog(int msg, int dir)
{
#ifdef __WIN32__
    OPENFILENAMEW ofn;
    BROWSEINFOW bInfo;
    LPITEMIDLIST lpItem;
    wchar_t szFn[PATH_MAX + FILENAME_MAX + 1], szTitle[256];

    memset(fn, 0, sizeof(fn));
    memset(&szFn,0,sizeof(szFn));
    MultiByteToWideChar(CP_UTF8, 0, lang[msg], -1, szTitle, sizeof(szTitle)-1);
    if(dir) {
        memset(&bInfo,0,sizeof(bInfo));
        bInfo.hwndOwner = hwnd;
        bInfo.lpszTitle = szTitle;
        bInfo.pszDisplayName = szFn;
        bInfo.ulFlags = BIF_RETURNONLYFSDIRS | (msg == SAVEDIR ? BIF_USENEWUI : BIF_NEWDIALOGSTYLE | BIF_NONEWFOLDERBUTTON);
        bInfo.iImage = -1;
        if((lpItem = SHBrowseForFolderW(&bInfo)) != NULL) {
            SHGetPathFromIDListW(lpItem, szFn);
            WideCharToMultiByte(CP_UTF8, 0, szFn, -1, fn, PATH_MAX + FILENAME_MAX, NULL, NULL);
        }
    } else {
        memset(&ofn,0,sizeof(ofn));
        ofn.lStructSize     = sizeof(ofn);
        ofn.hwndOwner       = hwnd;
        ofn.hInstance       = hWinInstance;
        ofn.lpstrFile       = szTitle;
        ofn.lpstrFile       = szFn;
        ofn.nMaxFile        = sizeof(szFn)-1;
        ofn.Flags           = OFN_EXPLORER | OFN_FILEMUSTEXIST;
        if(GetOpenFileNameW(&ofn))
            WideCharToMultiByte(CP_UTF8, 0, szFn, -1, fn, PATH_MAX + FILENAME_MAX, NULL, NULL);
    }
#else
    pid_t pid;
    void *chooser;
    void *handle;
    char *tmp1, *tmp2;
    tmp1 = mmap(NULL, sizeof(fn), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(!tmp1) return;
    memset(tmp1, 0, sizeof(fn));
    memset(fn, 0, sizeof(fn));
    if(!(pid = fork())) {
        handle = dlopen("libgtk-3.so", RTLD_LAZY | RTLD_LOCAL);
        if(handle) {
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#endif
            if( (gtk_init = dlsym(handle, "gtk_init")) &&
                (gtk_file_chooser_dialog_new = dlsym(handle, "gtk_file_chooser_dialog_new")) &&
                (gtk_dialog_run = dlsym(handle, "gtk_dialog_run")) &&
                (gtk_file_chooser_set_create_folders = dlsym(handle, "gtk_file_chooser_set_create_folders")) &&
                (gtk_file_chooser_set_do_overwrite_confirmation = dlsym(handle, "gtk_file_chooser_set_do_overwrite_confirmation")) &&
                (gtk_file_chooser_set_current_name = dlsym(handle, "gtk_file_chooser_set_current_name")) &&
                (gtk_file_chooser_get_filename = dlsym(handle, "gtk_file_chooser_get_filename")) &&
                (gtk_widget_destroy = dlsym(handle, "gtk_widget_destroy")) ) {
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
                (*gtk_init)(NULL, NULL);
                chooser = (*gtk_file_chooser_dialog_new)(lang[msg], NULL,
                    dir ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER : GTK_FILE_CHOOSER_ACTION_OPEN, "gtk-cancel", -6, "gtk-open", -3, NULL);
                if((*gtk_dialog_run)(chooser) == -3) {
                    tmp2 = (*gtk_file_chooser_get_filename)(chooser);
                    if(tmp2) strncpy(tmp1 + 1, tmp2, sizeof(fn) - 2);
                }
                (*gtk_widget_destroy)(chooser);
            } else fprintf(stderr, "unable to load GTK symbols\n");
            dlclose(handle);
        } else fprintf(stderr, "unable to run-time link the GTK3 library\n");
        tmp1[0] = 1;
        exit(0);
    }
    if(pid < 0) fprintf(stderr, "unable to fork\n");
    else {
        while(!tmp1[0] && !kill(pid, SIGCONT)) SDL_Delay(100);
        kill(pid, SIGKILL);
    }
    if(tmp1[1]) strcpy(fn, tmp1 + 1);
    munmap(tmp1, sizeof(fn));
#endif
}

/**
 * Open a save file modal
 */
void savedialog(char *path, char *name, uint8_t *buf, uint64_t size)
{
#ifdef __WIN32__
    OPENFILENAMEW ofn;
    wchar_t szPath[PATH_MAX + 1], szFn[FILENAME_MAX + 1], szExt[FILENAME_MAX + 1];
    int l = 0;

    if(!name || !*name || !buf || size < 1) return;
    memset(fn, 0, sizeof(fn));
    memset(&szFn,0,sizeof(szFn));
    memset(&szExt,0,sizeof(szExt));
    memcpy(szExt, L"All\0*.*\0", 18);
    if(path && *path) MultiByteToWideChar(CP_UTF8, 0, path, -1, szPath, sizeof(szPath)-1);
    l = MultiByteToWideChar(CP_UTF8, 0, name, -1, szFn, sizeof(szFn)-1);
    while(l > 0 && szFn[l - 1] != L'.') l--;
    if(l > 0) wsprintfW(szExt, L"%s\0*.%s\0", szFn + l, szFn + l);
    memset(&ofn,0,sizeof(ofn));
    ofn.lStructSize     = sizeof(ofn);
    ofn.hwndOwner       = hwnd;
    ofn.hInstance       = hWinInstance;
    if(path && *path) ofn.lpstrInitialDir = szPath;
    ofn.lpstrFilter     = szExt;
    ofn.lpstrFile       = szFn;
    ofn.nMaxFile        = sizeof(szFn)-1;
    ofn.lpstrDefExt     = l > 0 ? &szFn[l] : NULL;
    ofn.Flags           = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST;
    if(GetSaveFileNameW(&ofn))
        WideCharToMultiByte(CP_UTF8, 0, szFn, -1, fn, PATH_MAX + FILENAME_MAX + 1, NULL, NULL);
#else
    pid_t pid;
    void *chooser;
    void *handle;
    char *tmp1, *tmp2;

(void)path;
    if(!name || !*name || !buf || size < 1) return;
    tmp1 = mmap(NULL, sizeof(fn), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(!tmp1) return;
    memset(tmp1, 0, sizeof(fn));
    memset(fn, 0, sizeof(fn));
    if(!(pid = fork())) {
        handle = dlopen("libgtk-3.so", RTLD_LAZY | RTLD_LOCAL);
        if(handle) {
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#endif
            if( (gtk_init = dlsym(handle, "gtk_init")) &&
                (gtk_file_chooser_dialog_new = dlsym(handle, "gtk_file_chooser_dialog_new")) &&
                (gtk_dialog_run = dlsym(handle, "gtk_dialog_run")) &&
                (gtk_file_chooser_set_create_folders = dlsym(handle, "gtk_file_chooser_set_create_folders")) &&
                (gtk_file_chooser_set_do_overwrite_confirmation = dlsym(handle, "gtk_file_chooser_set_do_overwrite_confirmation")) &&
                (gtk_file_chooser_set_current_folder = dlsym(handle, "gtk_file_chooser_set_current_folder")) &&
                (gtk_file_chooser_set_current_name = dlsym(handle, "gtk_file_chooser_set_current_name")) &&
                (gtk_file_chooser_get_filename = dlsym(handle, "gtk_file_chooser_get_filename")) &&
                (gtk_widget_destroy = dlsym(handle, "gtk_widget_destroy")) ) {
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
                (*gtk_init)(NULL, NULL);
                chooser = (*gtk_file_chooser_dialog_new)(NULL, NULL, GTK_FILE_CHOOSER_ACTION_SAVE, "gtk-cancel", -6, "gtk-save", -3, NULL);
                (*gtk_file_chooser_set_create_folders)(chooser, 1);
                (*gtk_file_chooser_set_do_overwrite_confirmation)(chooser, 1);
                if(path && *path) (*gtk_file_chooser_set_current_folder)(chooser, path);
                (*gtk_file_chooser_set_current_name)(chooser, name);
                if((*gtk_dialog_run)(chooser) == -3) {
                    tmp2 = (*gtk_file_chooser_get_filename)(chooser);
                    if(tmp2) strncpy(tmp1 + 1, tmp2, sizeof(fn) - 2);
                }
                (*gtk_widget_destroy)(chooser);
            } else fprintf(stderr, "unable to load GTK symbols\n");
            dlclose(handle);
        } else fprintf(stderr, "unable to run-time link the GTK3 library\n");
        tmp1[0] = 1;
        exit(0);
    }
    if(pid < 0) fprintf(stderr, "unable to fork\n");
    else {
        while(!tmp1[0] && !kill(pid, SIGCONT)) SDL_Delay(100);
        kill(pid, SIGKILL);
    }
    if(tmp1[1]) strcpy(fn, tmp1 + 1);
    munmap(tmp1, sizeof(fn));
#endif
    if(fn[0])
        putfile(fn, buf, size, NULL, 0, NULL, 0);
}
